iT邦幫忙

2024 iThome 鐵人賽

DAY 21
1
JavaScript

Don't make JavaScript Just Surpise系列 第 21

例外(Exception)、錯誤物件(Error)與攔截

  • 分享至 

  • xImage
  •  

俗話說人無完人,程式碼也難有完美的程式碼,開發者的開發生活中與錯誤為伍的時間比例說不定和開發功能的時間也不少多少。

錯誤的種類

要說執行會遇到的錯誤,可以粗略分為三種。

  1. 語法錯誤:少一個括號之類的
    let foo = ;//Uncaught SyntaxError: Unexpected token ';'
    
  2. 邏輯錯誤:程式碼的撰寫過程中,沒有處理好部分例子,得到預期外的結果
  3. 執行時錯誤:對一個不當對象使用不合的方法,如對一個 undefined 進行屬性或方法的存取
    const getObj = ()=> undefined;
    let obj = getObj();
    console.log(obj.name);
    //Uncaught TypeError: Cannot read properties of undefined (reading 'name')
    

語法錯誤無法被我們撰寫的程式碼攔截,因為它發生在解析階段,一個程式碼執行前的階段。
如果語法錯誤發生,任何程式碼都不會執行,因為解析編譯沒有過。

邏輯錯誤需要更多的上下文,明確知道所有可能的輸入輸出,熟悉 JS 語法的使用、撰寫得當的測試案例來避免邏輯錯誤。但就算我們做得足夠好了,可能還是會有預想外的情況(比如呼叫遠端 API 規範說不會回傳 undefined,但對方就是丟了 undefined),那就會發生執行時錯誤,兩者是有關連的。

執行時錯誤指的就是程式碼層面在執行了沒有對應邏輯處理或禁止使用的例子,比如 undefined.nameTDZ。我們一般討論的就是捕捉執行時錯誤。

例外和錯誤(Exception and Erorr)

在 JS 中例外跟錯誤近乎是可以交替使用的。
因為 JS 中只有 Error 一個預留關鍵字,作為錯誤發生時承載資訊的物件。
習慣上我會更把 JS 中的例外當做廣泛情形表述用,比如拋出例外、處理例外,但這兩種情況下都可以把例外替換成錯誤,一般大家也是聽得懂。但如果說到物件,那只會說是錯誤物件,而不會說例外物件,就如上面說的,只有 Error 這個關鍵字。

一般來說,JS 中的錯誤物件皆會使用 Error

錯誤物件(Error)的結構

JS 的錯誤物件提供以下幾種建構方法:

new Error()
new Error(message)
new Error(message, options)
Error()
Error(message)
Error(message, options)

Error 物件在使用時,不管有沒有用 new 關鍵字,都會回傳一個全新建構的錯誤物件。

MDN 上還有一些非標準的屬性與對應建構方式,我就沒有列在這邊。透過上面的建構式,我們可以知道 JS 的錯誤物件建構最多會用到 messageoption,而被建構出來的物件會有以下幾個屬性:

  • name
  • message
  • cause
  • stack

name 並沒有從建構式上接收參數,預設會是 'Error' 字串,可以透過存取錯誤物件本身去修改該屬性。
message 會是從建構式上接收的參數字串,如無傳入,預設為空。
cause 比較特別,會被包在 options 中,建構時以 {cause:}帶入。用於顯示詳細的錯誤,如往外拋出的時候保留此層的錯誤物件,
stack 是一個尚未完全標準化的值,不要在正式環境依賴他來處理邏輯,而是僅使用它來除錯。一般會用各種形式來記錄並展示此次函式的錯誤 callstack,詳細一點可能包含方法名稱、行數列數。對於除錯來說,是一個很有用的屬性。

如果對錯誤物件本身用 toString() 方法,會拿到的是 name + message 的字串。

例外攔截

這邊只討論執行時錯誤(runtime error)。
如果一個例外發生,沒有任何的語句嘗試攔截,則該例外會持續冒泡(bubble up)(指的是往例外發生的外層上下文 context 傳遞,尋找有針對例外做處理的區塊,一旦找到就交由該區塊做處理)。若一直沒有找到能夠處理的對應區塊,一路到了程式的最上層,則再來會由該執行環境決定處理方法,如瀏覽器會停止在最後錯誤拋出的那行,印出訊息在開發者控制台(Developer Tool - Console),就不再繼續執行;Node.js 則是整個 Process 會崩潰。(這種稱作 未捕獲例外 Uncaught Error)

所以在有例外可能發生的情況下,我們應該妥善使用 try catch 語句來接住錯誤。
try{} 的區段中放入的程式碼,一旦有例外拋出,則會由 catch 接住,第一個輸入參數為例外的錯誤物件。

來看一個實際的例子:

function boilWater() {
  try {
    getSomeWater();
  } catch(err) {
    console.log("boilWater name", err.name);
    console.log("boilWater stack", err.stack);
    console.log("boilWater message", err.message);
    console.log("boilWater cause", err.cause);
  }
}
function getSomeWater() {
  try {
    throw new Error();
    throw new Error("No Water");
  } catch(err) {
    console.log("getSomeWater name", err.name);
    console.log("getSomeWater message", err.message);
    console.log("getSomeWater stack", err.stack);
    console.log("getSomeWater cause", err.cause);
    throw new Error(err, { cause: err });
  }
}
boilWater();

印出的結果會是:

// "getSomeWater name", "Error"
// "getSomeWater message", ""
// "getSomeWater stack", "Error
//     at getSomeWater (https://fiddle.jshell.net/_display/?editor_console=true:129:11)
//     at boilWater (https://fiddle.jshell.net/_display/?editor_console=true:119:5)
//     at https://fiddle.jshell.net/_display/?editor_console=true:139:1"
// "getSomeWater cause", undefined

// "boilWater name", "Error"
// "boilWater stack", "Error: Error
//     at getSomeWater (https://fiddle.jshell.net/_display/?editor_console=true:136:11)
//     at boilWater (https://fiddle.jshell.net/_display/?editor_console=true:119:5)
//     at https://fiddle.jshell.net/_display/?editor_console=true:139:1"
// "boilWater message", "Error"
// "boilWater cause", [object Error] { ... }

例外從內層往外拋,執行的順序是 boilWater() -> getSomeWater() -> getSomeWater 的例外,所以遇到例外就往回開始逐層傳遞(Stack 的概念,後進先出)。

因為 getSomeWater() 中就有 try catch 把錯誤接了下來,所以我們可以看到錯誤的資訊。
cause 在這個例子中是 undefined,因為我們沒有特別用 {cause:} 去給值。

再來看持續往外拋,這次有用 {cause:} 給值,所以可以看到外層的 cause 就有接到傳遞出來的內層錯誤物件。
stack 中表示的字串就敘述了這樣的呼叫順序,上到下會是內到外,136:11 表示的是錯誤語句對應的 行數:列數,有助於我們找到語句發生錯誤的地方。

try{}catch{}語句還能接上 finally{} 子句,用於執行無論是否發生例外,這塊主要用於處理一些無論是否正常運作都要處理的事項,如關閉連線,給予回傳,記憶體相關處理等等。
要盡量避免 finally 區塊裡發生錯誤,如果有錯誤發生,則一樣會往外拋。

try相關語句的執行順序如下:

  1. 無例外:try -> finally
  2. 有例外: try -> try 發生錯誤的那行 -> catch -> finally

錯誤種類

大多數情況下內建的錯誤類別給的訊息其實都蠻足夠的,作為一個開發者,要習慣去閱讀拿到的錯誤訊息。

Uncaught TypeError: Cannot read properties of undefined (reading 'name')

比如這段說的就是有一個沒接到(Uncaught)的 TypeErorr 型別錯誤,無法從 undefined 上閱讀屬性,而被閱讀的屬性是 name

MDN 上有提供一份常見的錯誤列表。
錯誤的名字一般多能用來分類大概的錯誤類型,幫助我們能快速了解大概是發生了什麼問題: Type Erorr 是針對型別的不當使用,Syntax Error 是語法使用錯誤,Range Error 是型別處理的數值不合法或超出範圍,Reference Error 是你嘗試使用一個不合法的對象(如對未經宣告的變數做了 RHS)。

通常錯誤名字訊息一查,就會看到千百個踩過坑的人問了千百個問題,找到錯誤解釋不難,但要配合 stack 去確認你的錯誤發生點,才能更精準的定位自己程式碼裡錯誤發生的原因。


以上是錯誤的物件與機制介紹,好好接下錯誤,考慮例外的情況,才能讓程式更穩妥的運行。


上一篇
陣列(Array)與相關操作
下一篇
代理物件(Proxy)
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言